// Copyright Epic Games, Inc. All Rights Reserved.
// universal module definition - read https://www.davidbcalhoun.com/2014/what-is-amd-commonjs-and-umd/

export function webRtcPlayer(parOptions) {
  parOptions = typeof parOptions !== "undefined" ? parOptions : {};

  var self = this;

  //**********************
  //Config setup
  //**********************
  this.cfg =
    typeof parOptions.peerConnectionOptions !== "undefined"
      ? parOptions.peerConnectionOptions
      : {};
  this.cfg.sdpSemantics = "unified-plan";
  // this.cfg.rtcAudioJitterBufferMaxPackets = 10;
  // this.cfg.rtcAudioJitterBufferFastAccelerate = true;
  // this.cfg.rtcAudioJitterBufferMinDelayMs = 0;

  // If this is true in Chrome 89+ SDP is sent that is incompatible with UE Pixel Streaming 4.26 and below.
  // However 4.27 Pixel Streaming does not need this set to false as it supports `offerExtmapAllowMixed`.
  // tdlr; uncomment this line for older versions of Pixel Streaming that need Chrome 89+.
  this.cfg.offerExtmapAllowMixed = false;

  //**********************
  //Variables
  //**********************
  this.pcClient = null;
  this.dcClient = null;
  this.tnClient = null;

  this.sdpConstraints = {
    offerToReceiveAudio: 1, //Note: if you don't need audio you can get improved latency by turning this off.
    offerToReceiveVideo: 1,
    voiceActivityDetection: false,
  };

  // See https://www.w3.org/TR/webrtc/#dom-rtcdatachannelinit for values (this is needed for Firefox to be consistent with Chrome.)
  this.dataChannelOptions = { ordered: true };

  // This is useful if the video/audio needs to autoplay (without user input) as browsers do not allow autoplay non-muted of sound sources without user interaction.
  this.startVideoMuted =
    typeof parOptions.startVideoMuted !== "undefined"
      ? parOptions.startVideoMuted
      : false;
  this.autoPlayAudio =
    typeof parOptions.autoPlayAudio !== "undefined"
      ? parOptions.autoPlayAudio
      : true;

  // To enable mic in browser use SSL/localhost and have ?useMic in the query string.
  const urlParams = new URLSearchParams(window.location.search);
  this.useMic = urlParams.has("useMic");
  if (!this.useMic) {
    console.log(
      "Microphone access is not enabled. Pass ?useMic in the url to enable it."
    );
  }

  // When ?useMic check for SSL or localhost
  let isLocalhostConnection =
    location.hostname === "localhost" || location.hostname === "127.0.0.1";
  let isHttpsConnection = location.protocol === "https:";
  if (this.useMic && !isLocalhostConnection && !isHttpsConnection) {
    this.useMic = false;
    console.error(
      "Microphone access in the browser will not work if you are not on HTTPS or localhost. Disabling mic access."
    );
    console.error(
      "For testing you can enable HTTP microphone access Chrome by visiting chrome://flags/ and enabling 'unsafely-treat-insecure-origin-as-secure'"
    );
  }

  // Latency tester
  this.latencyTestTimings = {
    TestStartTimeMs: null,
    UEReceiptTimeMs: null,
    UEPreCaptureTimeMs: null,
    UEPostCaptureTimeMs: null,
    UEPreEncodeTimeMs: null,
    UEPostEncodeTimeMs: null,
    UETransmissionTimeMs: null,
    BrowserReceiptTimeMs: null,
    FrameDisplayDeltaTimeMs: null,
    Reset: function () {
      this.TestStartTimeMs = null;
      this.UEReceiptTimeMs = null;
      this.UEPreCaptureTimeMs = null;
      this.UEPostCaptureTimeMs = null;
      this.UEPreEncodeTimeMs = null;
      this.UEPostEncodeTimeMs = null;
      this.UETransmissionTimeMs = null;
      this.BrowserReceiptTimeMs = null;
      this.FrameDisplayDeltaTimeMs = null;
    },
    SetUETimings: function (UETimings) {
      this.UEReceiptTimeMs = UETimings.ReceiptTimeMs;
      this.UEPreCaptureTimeMs = UETimings.PreCaptureTimeMs;
      this.UEPostCaptureTimeMs = UETimings.PostCaptureTimeMs;
      this.UEPreEncodeTimeMs = UETimings.PreEncodeTimeMs;
      this.UEPostEncodeTimeMs = UETimings.PostEncodeTimeMs;
      this.UETransmissionTimeMs = UETimings.TransmissionTimeMs;
      this.BrowserReceiptTimeMs = Date.now();
      this.OnAllLatencyTimingsReady(this);
    },
    SetFrameDisplayDeltaTime: function (DeltaTimeMs) {
      if (this.FrameDisplayDeltaTimeMs == null) {
        this.FrameDisplayDeltaTimeMs = Math.round(DeltaTimeMs);
        this.OnAllLatencyTimingsReady(this);
      }
    },
    OnAllLatencyTimingsReady: function (Timings) {},
  };

  //**********************
  //Functions
  //**********************

  //Create Video element and expose that as a parameter
  this.createWebRtcVideo = function () {
    // var video = document.createElement("video");
    // video.id = "streamingVideo";
    // video.style = "width:100%; height:100%";
    // video.disablepictureinpicture = true;
    // video.playsInline = true;
    var video = document.getElementById("streamingVideo");
    video.muted = "muted";
    video.disablepictureinpicture = true;
    // video.object-fit = 'fill';
    video.addEventListener(
      "loadedmetadata",
      function (e) {
        if (self.onVideoInitialised) {
          self.onVideoInitialised();
        }
      },
      true
    );

    // Check if request video frame callback is supported
    if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) {
      // The API is supported!

      const onVideoFrameReady = (now, metadata) => {
        if (metadata.receiveTime && metadata.expectedDisplayTime) {
          const receiveToCompositeMs =
            metadata.presentationTime - metadata.receiveTime;
          self.aggregatedStats.receiveToCompositeMs = receiveToCompositeMs;
        }

        // Re-register the callback to be notified about the next frame.
        video.requestVideoFrameCallback(onVideoFrameReady);
      };

      // Initially register the callback to be notified about the first frame.
      video.requestVideoFrameCallback(onVideoFrameReady);
    }

    return video;
  };

  this.video = this.createWebRtcVideo();

  let onsignalingstatechange = function (state) {
    console.info("signaling state change:", state);
  };

  let oniceconnectionstatechange = function (state) {
    console.info("ice connection state change:", state);
  };

  let onicegatheringstatechange = function (state) {
    console.info("ice gathering state change:", state);
  };

  let handleOnTrack = function (e) {
    console.log("handleOnTrack", e.streams);
    console.log("🚀 🚀 🚀 🚀 🚀 🚀 e", e);
    console.log(
      "🚀 🚀 🚀 🚀 🚀 🚀 self.video.srcObject ",
      self.video,
      self.video.srcObject
    );
    if (e.track) {
      console.log(
        "Got track - " +
          e.track.kind +
          " id=" +
          e.track.id +
          " readyState=" +
          e.track.readyState
      );
    }

    if (e.track.kind == "audio") {
      console.log(
        "🚀 ~ file: webRtcPlayer.js:207 ~ handleOnTrack ~ e.track.kind",
        e.track
      );
      handleOnAudioTrack(e.streams[0]);
      return;
    } else e.track.kind == "video" && self.video.srcObject !== e.streams[0];
    {
      self.video.srcObject = e.streams[0];
      console.log("Set video source from video track ontrack.");
      return;
    }
  };

  let handleOnAudioTrack = function (audioMediaStream) {
    // do nothing the video has the same media stream as the audio track we have here (they are linked)
    if (self.video.srcObject == audioMediaStream) {
      return;
    }
    // video element has some other media stream that is not associated with this audio track
    else if (
      self.video.srcObject &&
      self.video.srcObject !== audioMediaStream
    ) {
      // create a new audio element
      let audioElem = document.createElement("Audio");
      audioElem.srcObject = audioMediaStream;

      // there is no way to autoplay audio (even muted), so we defer audio until first click
      console.log("self ", self);
      if (!self.autoPlayAudio) {
        let clickToPlayAudio = function () {
          audioElem.play();
          self.video.removeEventListener("click", clickToPlayAudio);
        };

        self.video.addEventListener("click", clickToPlayAudio);
      }
      // we assume the user has clicked somewhere on the page and autoplaying audio will work
      else {
        // audioElem.play();
      }
      console.log("Created new audio element to play seperate audio stream.");
    }
  };

  let setupDataChannel = function (pc, label, options) {
    try {
      let datachannel = pc.createDataChannel(label, options);
      console.log(`Created datachannel (${label})`);

      // Inform browser we would like binary data as an ArrayBuffer (FF chooses Blob by default!)
      datachannel.binaryType = "arraybuffer";

      datachannel.onopen = function (e) {
        console.log(`data channel (${label}) connect`);
        if (self.onDataChannelConnected) {
          self.onDataChannelConnected();
        }
      };

      datachannel.onclose = function (e) {
        console.log(`data channel (${label}) closed`);
      };

      datachannel.onmessage = function (e) {
        //console.log(`Got message (${label})`, e.data)
        if (self.onDataChannelMessage) self.onDataChannelMessage(e.data);
      };

      return datachannel;
    } catch (e) {
      console.warn("No data channel", e);
      return null;
    }
  };

  let onicecandidate = function (e) {
    console.log("ICE candidate", e);
    if (e.candidate && e.candidate.candidate) {
      self.onWebRtcCandidate(e.candidate);
    }
  };

  let handleCreateOffer = function (pc) {
    pc.createOffer(self.sdpConstraints).then(
      function (offer) {
        // Munging is where we modifying the sdp string to set parameters that are not exposed to the browser's WebRTC API
        mungeSDPOffer(offer);

        // Set our munged SDP on the local peer connection so it is "set" and will be send across
        pc.setLocalDescription(offer);
        if (self.onWebRtcOffer) {
          self.onWebRtcOffer(offer);
        }
      },
      function () {
        console.warn("Couldn't create offer");
      }
    );
  };

  let mungeSDPOffer = function (offer) {
    // turn off video-timing sdp sent from browser
    //offer.sdp = offer.sdp.replace("http://www.webrtc.org/experiments/rtp-hdrext/playout-delay", "");

    // this indicate we support stereo (Chrome needs this)
    offer.sdp = offer.sdp.replace(
      "useinbandfec=1",
      "useinbandfec=1;stereo=1;sprop-maxcapturerate=48000"
    );
  };

  let setupPeerConnection = function (pc) {
    if (pc.SetBitrate)
      console.log("Hurray! there's RTCPeerConnection.SetBitrate function");

    //Setup peerConnection events
    pc.onsignalingstatechange = onsignalingstatechange;
    pc.oniceconnectionstatechange = oniceconnectionstatechange;
    pc.onicegatheringstatechange = onicegatheringstatechange;

    pc.ontrack = handleOnTrack;
    pc.onicecandidate = onicecandidate;
  };

  let generateAggregatedStatsFunction = function () {
    if (!self.aggregatedStats) self.aggregatedStats = {};

    return function (stats) {
      //console.log('Printing Stats');

      let newStat = {};

      stats.forEach((stat) => {
        //                    console.log(JSON.stringify(stat, undefined, 4));
        if (
          stat.type == "inbound-rtp" &&
          !stat.isRemote &&
          (stat.mediaType == "video" || stat.id.toLowerCase().includes("video"))
        ) {
          newStat.timestamp = stat.timestamp;
          newStat.bytesReceived = stat.bytesReceived;
          newStat.framesDecoded = stat.framesDecoded;
          newStat.packetsLost = stat.packetsLost;
          newStat.bytesReceivedStart =
            self.aggregatedStats && self.aggregatedStats.bytesReceivedStart
              ? self.aggregatedStats.bytesReceivedStart
              : stat.bytesReceived;
          newStat.framesDecodedStart =
            self.aggregatedStats && self.aggregatedStats.framesDecodedStart
              ? self.aggregatedStats.framesDecodedStart
              : stat.framesDecoded;
          newStat.timestampStart =
            self.aggregatedStats && self.aggregatedStats.timestampStart
              ? self.aggregatedStats.timestampStart
              : stat.timestamp;

          if (self.aggregatedStats && self.aggregatedStats.timestamp) {
            if (self.aggregatedStats.bytesReceived) {
              // bitrate = bits received since last time / number of ms since last time
              //This is automatically in kbits (where k=1000) since time is in ms and stat we want is in seconds (so a '* 1000' then a '/ 1000' would negate each other)
              newStat.bitrate =
                (8 *
                  (newStat.bytesReceived -
                    self.aggregatedStats.bytesReceived)) /
                (newStat.timestamp - self.aggregatedStats.timestamp);
              newStat.bitrate = Math.floor(newStat.bitrate);
              newStat.lowBitrate =
                self.aggregatedStats.lowBitrate &&
                self.aggregatedStats.lowBitrate < newStat.bitrate
                  ? self.aggregatedStats.lowBitrate
                  : newStat.bitrate;
              newStat.highBitrate =
                self.aggregatedStats.highBitrate &&
                self.aggregatedStats.highBitrate > newStat.bitrate
                  ? self.aggregatedStats.highBitrate
                  : newStat.bitrate;
            }

            if (self.aggregatedStats.bytesReceivedStart) {
              newStat.avgBitrate =
                (8 *
                  (newStat.bytesReceived -
                    self.aggregatedStats.bytesReceivedStart)) /
                (newStat.timestamp - self.aggregatedStats.timestampStart);
              newStat.avgBitrate = Math.floor(newStat.avgBitrate);
            }

            if (self.aggregatedStats.framesDecoded) {
              // framerate = frames decoded since last time / number of seconds since last time
              newStat.framerate =
                (newStat.framesDecoded - self.aggregatedStats.framesDecoded) /
                ((newStat.timestamp - self.aggregatedStats.timestamp) / 1000);
              newStat.framerate = Math.floor(newStat.framerate);
              newStat.lowFramerate =
                self.aggregatedStats.lowFramerate &&
                self.aggregatedStats.lowFramerate < newStat.framerate
                  ? self.aggregatedStats.lowFramerate
                  : newStat.framerate;
              newStat.highFramerate =
                self.aggregatedStats.highFramerate &&
                self.aggregatedStats.highFramerate > newStat.framerate
                  ? self.aggregatedStats.highFramerate
                  : newStat.framerate;
            }

            if (self.aggregatedStats.framesDecodedStart) {
              newStat.avgframerate =
                (newStat.framesDecoded -
                  self.aggregatedStats.framesDecodedStart) /
                ((newStat.timestamp - self.aggregatedStats.timestampStart) /
                  1000);
              newStat.avgframerate = Math.floor(newStat.avgframerate);
            }
          }
        }

        //Read video track stats
        if (
          stat.type == "track" &&
          (stat.trackIdentifier == "video_label" || stat.kind == "video")
        ) {
          newStat.framesDropped = stat.framesDropped;
          newStat.framesReceived = stat.framesReceived;
          newStat.framesDroppedPercentage =
            (stat.framesDropped / stat.framesReceived) * 100;
          newStat.frameHeight = stat.frameHeight;
          newStat.frameWidth = stat.frameWidth;
          newStat.frameHeightStart =
            self.aggregatedStats && self.aggregatedStats.frameHeightStart
              ? self.aggregatedStats.frameHeightStart
              : stat.frameHeight;
          newStat.frameWidthStart =
            self.aggregatedStats && self.aggregatedStats.frameWidthStart
              ? self.aggregatedStats.frameWidthStart
              : stat.frameWidth;
        }

        if (
          stat.type == "candidate-pair" &&
          stat.hasOwnProperty("currentRoundTripTime") &&
          stat.currentRoundTripTime != 0
        ) {
          newStat.currentRoundTripTime = stat.currentRoundTripTime;
        }
      });

      if (self.aggregatedStats.receiveToCompositeMs) {
        newStat.receiveToCompositeMs =
          self.aggregatedStats.receiveToCompositeMs;
        self.latencyTestTimings.SetFrameDisplayDeltaTime(
          self.aggregatedStats.receiveToCompositeMs
        );
      }

      self.aggregatedStats = newStat;

      if (self.onAggregatedStats) self.onAggregatedStats(newStat);
    };
  };

  let setupTracksToSendAsync = async function (pc) {
    // Setup a transceiver for getting UE video
    // 设置用于获取UE视频的收发器
    pc.addTransceiver("video", { direction: "recvonly" });

    // Setup a transceiver for sending mic audio to UE and receiving audio from UE
    // 设置收发器，用于向UE发送麦克风音频并从UE接收音频
    if (!self.useMic) {
      pc.addTransceiver("audio", { direction: "recvonly" });
    } else {
      let audioSendOptions = self.useMic
        ? {
            autoGainControl: false,
            channelCount: 1,
            echoCancellation: false,
            latency: 0,
            noiseSuppression: false,
            sampleRate: 16000,
            volume: 1.0,
          }
        : false;

      // Note using mic on android chrome requires SSL or chrome://flags/ "unsafely-treat-insecure-origin-as-secure"
      const stream = await navigator.mediaDevices.getUserMedia({
        video: false,
        audio: audioSendOptions,
      });
      if (stream) {
        for (const track of stream.getTracks()) {
          if (track.kind && track.kind == "audio") {
            pc.addTransceiver(track, { direction: "sendrecv" });
          }
        }
      } else {
        pc.addTransceiver("audio", { direction: "recvonly" });
      }
    }
  };

  //**********************
  //Public functions
  //**********************

  this.setVideoEnabled = function (enabled) {
    self.video.srcObject
      .getTracks()
      .forEach((track) => (track.enabled = enabled));
  };

  this.startLatencyTest = function (onTestStarted) {
    // Can't start latency test without a video element
    if (!self.video) {
      return;
    }

    self.latencyTestTimings.Reset();
    self.latencyTestTimings.TestStartTimeMs = Date.now();
    onTestStarted(self.latencyTestTimings.TestStartTimeMs);
  };

  //This is called when revceiving new ice candidates individually instead of part of the offer
  //This is currently not used but would be called externally from this class
  this.handleCandidateFromServer = function (iceCandidate) {
    console.log("ICE candidate: ", iceCandidate);
    let candidate = new RTCIceCandidate(iceCandidate);
    self.pcClient.addIceCandidate(candidate).then((_) => {
      console.log("ICE candidate successfully added");
    });
  };

  //Called externaly to create an offer for the server
  this.createOffer = function () {
    if (self.pcClient) {
      console.log("Closing existing PeerConnection");
      self.pcClient.close();
      self.pcClient = null;
    }
    self.pcClient = new RTCPeerConnection(self.cfg);

    setupTracksToSendAsync(self.pcClient).finally(function () {
      setupPeerConnection(self.pcClient);
      self.dcClient = setupDataChannel(
        self.pcClient,
        "cirrus",
        self.dataChannelOptions
      );
      handleCreateOffer(self.pcClient);
    });
  };

  //Called externaly when an answer is received from the server
  this.receiveAnswer = function (answer) {
    console.log("Received answer:");
    console.log(answer);
    var answerDesc = new RTCSessionDescription(answer);
    self.pcClient.setRemoteDescription(answerDesc);

    let receivers = self.pcClient.getReceivers();
    for (let receiver of receivers) {
      receiver.playoutDelayHint = 0;
    }
  };

  this.close = function () {
    if (self.pcClient) {
      console.log("Closing existing peerClient");
      self.pcClient.close();
      self.pcClient = null;
    }
    if (self.aggregateStatsIntervalId)
      clearInterval(self.aggregateStatsIntervalId);
  };

  //Sends data across the datachannel
  this.send = function (data) {
    if (self.dcClient && self.dcClient.readyState == "open") {
      //console.log('Sending data on dataconnection', self.dcClient)
      self.dcClient.send(data);
    }
  };

  this.getStats = function (onStats) {
    if (self.pcClient && onStats) {
      self.pcClient.getStats(null).then((stats) => {
        onStats(stats);
      });
    }
  };

  this.aggregateStats = function (checkInterval) {
    let calcAggregatedStats = generateAggregatedStatsFunction();
    let printAggregatedStats = () => {
      self.getStats(calcAggregatedStats);
    };
    self.aggregateStatsIntervalId = setInterval(
      printAggregatedStats,
      checkInterval
    );
  };
}
